AbstractCycleLoader.java
package org.codefilarete.stalactite.engine.runtime.cycle;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.codefilarete.stalactite.engine.EntityPersister;
import org.codefilarete.stalactite.engine.configurer.CascadeConfigurationResult;
import org.codefilarete.stalactite.engine.configurer.onetoone.FirstPhaseCycleLoadListener;
import org.codefilarete.stalactite.engine.listener.SelectListener;
import org.codefilarete.stalactite.sql.result.BeanRelationFixer;
import org.codefilarete.tool.VisibleForTesting;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.trace.MutableInt;
/**
* Abstraction to handle graph loading while it contains a cycle in mapping definition.
* Mechanism is made of recursion through {@link SelectListener} combined with {@link FirstPhaseCycleLoadListener}, which actually triggers
* very first iteration while whole graph is loaded.
* This class must be Thread-safe because its a singleton from a configuration point of view, and, as a {@link SelectListener}, it is invoked
* by several threads.
*
* @author Guillaume Mary
*/
public abstract class AbstractCycleLoader<SRC, TRGT, TRGTID> implements SelectListener<TRGT, TRGTID> {
protected final EntityPersister<TRGT, TRGTID> targetPersister;
/**
* Relations to be fulfilled.
* Stored by their path in the graph (please note that the only expected thing here is the uniqueness, being the path in the graph fills this goal
* and was overall chosen for debugging purpose)
*/
protected final Map<String, CascadeConfigurationResult<SRC, TRGT>> relations = new HashMap<>();
protected final ThreadContext<CycleLoadRuntimeContext<SRC, TRGTID>> currentRuntimeContext = new ThreadContext<>(CycleLoadRuntimeContext::new);
/**
* Stores loaded entities. Made to avoid loading too many entities and making job done twice, as well as to avoid
* infinite loop if database contains cycling (non tree) data
*/
@VisibleForTesting
final ThreadContext<Set<TRGTID>> currentlyLoadedEntityIdsInCycle = new ThreadContext<>(HashSet::new);
/**
* Helper to detect end of beforeSelect / select / afterSelect loop. Made to clear other ThreadLocal fields.
*/
@VisibleForTesting
final ThreadContext<MutableInt> currentCycleCount = new ThreadContext<>(MutableInt::new);
protected AbstractCycleLoader(EntityPersister<TRGT, TRGTID> targetPersister) {
this.targetPersister = targetPersister;
}
public void addRelation(String relationIdentifier, CascadeConfigurationResult<SRC, TRGT> configurationResult) {
this.relations.put(relationIdentifier, configurationResult);
}
@Override
public void beforeSelect(Iterable<TRGTID> ids) {
currentCycleCount.get().increment();
}
@Override
public void afterSelect(Set<? extends TRGT> result) {
Set<TRGTID> alreadyLoadedEntities = this.currentlyLoadedEntityIdsInCycle.get();
result.forEach(entity -> alreadyLoadedEntities.add(targetPersister.getId(entity)));
CycleLoadRuntimeContext<SRC, TRGTID> runtimeContext = this.currentRuntimeContext.get();
// We clear context field to avoid keeping previous result in memory which may generate some infinite loop
// when this.targetPersister is same instance as the one that triggered this method
// Moreover we only need this.currentRuntimeContext to be accessible from outside and ThreadSafe, acting as a
// data feeder for this method, so it is no more useful after that current method has read its content.
this.currentRuntimeContext.remove();
// we remove already loaded elements
Set<TRGTID> identifiersToLoad = runtimeContext.giveIdentifiersToLoad();
identifiersToLoad.removeAll(alreadyLoadedEntities);
if (!identifiersToLoad.isEmpty()) {
// WARN : this select will be recursive in cycle case : if targetPersister is same as source one or owns a relation of same type as source one
// Hence, targetPersister.select(..) will trigger this afterSelect() method again, so code right after select(..) will be executed
Set<TRGT> targets = targetPersister.select(identifiersToLoad);
Map<TRGTID, TRGT> targetPerId = Iterables.map(targets, targetPersister::getId);
relations.forEach((relationName, configurationResult) -> {
EntityRelationStorage<SRC, TRGTID> targetIdsPerSource = runtimeContext.getEntitiesToFulFill(relationName);
if (targetIdsPerSource != null) {
applyRelationToSource(targetIdsPerSource, configurationResult.getBeanRelationFixer(), targetPerId);
}
});
}
if (currentCycleCount.get().decrement() == 0) {
this.currentlyLoadedEntityIdsInCycle.remove();
this.currentRuntimeContext.remove();
this.currentCycleCount.remove();
}
}
protected abstract void applyRelationToSource(EntityRelationStorage<SRC, TRGTID> targetIdsPerSource,
BeanRelationFixer<SRC, TRGT> beanRelationFixer,
Map<TRGTID, TRGT> targetPerId);
@Override
public void onSelectError(Iterable<TRGTID> ids, RuntimeException exception) {
this.currentRuntimeContext.remove();
throw exception;
}
/**
* Wrapper around a {@link ThreadLocal} made to add {@link #isPresent()} to it. Else it is not possible to know
* if a {@link ThreadLocal} contains value while it has been created with {@link ThreadLocal#withInitial(Supplier)}.
* Please note that {@link #isPresent()} is only necessary for test assertion.
*
* @param <T> stored object type
*/
@VisibleForTesting
static class ThreadContext<T> {
private final ThreadLocal<T> store = new ThreadLocal<>();
private final Supplier<T> valueInitializer;
public ThreadContext() {
this(null);
}
public ThreadContext(Supplier<T> valueInitializer) {
this.valueInitializer = valueInitializer;
}
protected void set(T t) {
store.set(t);
}
public T get() {
if (!isPresent() && valueInitializer != null) {
store.set(valueInitializer.get());
}
return store.get();
}
public boolean isPresent() {
return store.get() != null;
}
public void remove() {
store.remove();
}
}
}